
本篇介紹 ES2020 (ES11) 提供的 String.prototype.matchAll()。
若將一個字串使用的 RegExp (regular expression,正規表達式,正規表示式) 設定了 sticky 或 global flag,則可能會有多個 capture groups,常見的情境會想迭代所有 match 到的結果,可能會有幾種作法:
String.prototype.match()
RegExp.prototype.exec()
String.prototype.replace()
分別來介紹過去的這些作法有哪些缺點。
String.prototype.match()若在 String.prototype.match() 使用的 RegExp 沒有設定 global flag,就只能取得第一個 capture group、index、input 和 groups 這些資訊:
let string = 'JavaScript ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/;
let result = string.match(pattern);
console.log(result);
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
若有 global flag 不是取得所有 capture group 和其他資訊,而是只能取得所有 match 到的字串:
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let result = string.match(pattern);
console.log(result);
// ["ES7", "ES8", "ES9"]
這樣根本不夠用!這就是 String.prototype.match() 可惜的地方。
如果你只想取得所有 match 到的字串,那 String.prototype.match() 很好用,若你想要的是詳細一點的資訊,例如:capture group,String.prototype.match() 是無法滿足你的。
那改用 RegExp.prototype.exec() 呢?
RegExp.prototype.exec()若在 RegExp.prototype.exec() 使用的 RegExp 有設定 global 或 sticky flag,在執行 RegExp.prototype.exec() 後,會在該 RegExp 物件儲存前一個 match 的 lastIndex (即上次最後 match 的字串的最後一個字元在原字串中的 index 為何,用於下一次 match 開始的 index)。
所以只要重複執行幾次 RegExp.prototype.exec(),就能一直取得 match 的結果,即取得第一個 capture group、index、input 和 groups 這些資訊。
直到 match 的結果為 null 時,代表已經找不到 match 的字串,此時會將 RegExp 物件的 lastIndex 設為 0,代表之後執行 RegExp.prototype.exec() 會重頭開始 match 字串。
用剛剛的範例來舉例:match 到幾個字串就要跑幾次,每次都會更新 RegExp 物件的 lastIndex:
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
console.log(pattern.lastIndex);
// 0
console.log(pattern.exec(string));
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
console.log(pattern.lastIndex);
// 3
console.log(pattern.exec(string));
// ["ES8", "ES8", "8", index: 4, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
console.log(pattern.lastIndex);
// 7
console.log(pattern.exec(string));
// ["ES9", "ES9", "9", index: 8, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
console.log(pattern.lastIndex);
// 11
console.log(pattern.exec(string));
// null
console.log(pattern.lastIndex);
// 0
若 RegExp.prototype.exec() 回傳為 null,且 RegExp 物件的 lastIndex 為 0 時,你再次執行 RegExp.prototype.exec() 就會重頭開始 match 字串:
console.log(pattern.exec(string));
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
console.log(pattern.lastIndex);
// 3
看到上面手動一步一步執行 RegExp.prototype.exec() 感到累嗎?用迴圈改寫一下:
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let match;
while (match = pattern.exec(string)) {
console.log(match);
}
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// ["ES8", "ES8", "8", index: 4, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// ["ES9", "ES9", "9", index: 8, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
舒服多了!
若要保存每次執行 RegExp.prototype.exec() 回傳的 match 結果,可能會這樣寫:
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let match;
let matches = [];
while (match = pattern.exec(string)) {
matches.push(match);
}
console.log(matches);
// [
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined],
// ["ES8", "ES8", "8", index: 4, input: "ES7 ES8 ES9 ECMAScript", groups: undefined],
// ["ES9", "ES9", "9", index: 8, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// ]
到這邊你覺得 RegExp.prototype.exec() 還行嗎?其實有一些小缺點!
RegExp.prototype.exec() 的小缺點小缺點如下:
RegExp.prototype.exec() 會改變 RegExp 物件的 lastIndex
先來說第一個小缺點:為了取得每次 RegExp.prototype.exec() 的 match 結果,且因需要設定迴圈的中止條件,要將 match 結果存在一個變數,這個變數宣告是為了 RegExp.prototype.exec() 的行為而建立的變數 (即下面的 match 變數):
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let match;
while (match = pattern.exec(string)) {
console.log(match);
}
逼不得已啊...。那能改用 for-of 嗎?這樣就不會多宣告變數啦!
RegExp.prototype.exec() 是不可能做到的,而本篇要介紹的 String.prototype.matchAll() 就能解決這個問題,後面會提到。
接著來說第二個小缺點:執行 RegExp.prototype.exec() 會改變 RegExp 物件的 lastIndex。
這看似沒什麼問題啊?其實問題會發生在不懂 RegExp.prototype.exec() 的人。
假設 RegExp pattern 是共用的,需要讓很多字串 match (此範例為 string1 和 string2 ):
string1 使用 RegExp.prototype.exec()
string2 使用 RegExp.prototype.exec()
let string1 = 'ES7 ES8 ES9 ECMAScript';
let string2 = 'ES10 ES11 ECMAScript';
let pattern = /(ES(\d+))/g;
console.log(pattern.lastIndex);
// 0
console.log(pattern.exec(string1));
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
console.log(pattern.lastIndex);
// 3
console.log(pattern.exec(string2));
// ["ES11", "ES11", "11", index: 5, input: "ES10 ES11 ECMAScript", groups: undefined]
console.log(pattern.lastIndex);
// 9
發現問題了嗎?不同字串共用的 RegExp pattern 會因為被修改了 lastIndex,而造成拿到的結果不符合你的預期,string2 看起來是第一次用該 RegExp pattern 來 match 字串,但卻拿到第二次才會被 match 的字串 (即 ES11,原本以為會拿到 ES10 )。
如果你熟悉
RegExp.prototype.exec(),這個問題根本不會發生,但過去還是新手的我就採過這個雷 XD
String.prototype.replace()有些情境需要透過 String.prototype.replace() 和 RegExp 來將某些字串取代成其他內容。
例如:將 ES7 轉成 ES2016,ES8 轉成 ES2017,ES9 轉成 ES2018,只有前綴 ES 後面加上數字字元才能轉換:
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let newString = string.replace(pattern, function(matched, position1, position2) {
const version = position2;
return `ES${2009 + Number(version)}`;
});
console.log(newString);
// ES2016 ES2017 ES2018 ECMAScript
註:
String.prototype.replace()的第二個參數可以是字串,或是 callback function,而 callback function 會有多個參數,包括:matched(match 到的字串)、positionN(第幾個 capture group)、index(match 到字元的 index) 和input(正在 match 的整個字串)。string.replace(pattern, function(matched, position1, ...positionN, index, input) { // ... });
若要讓 String.prototype.replace() 的行為很像 RegExp.prototype.exec() 回傳的 match 結果,可以這樣寫:
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let matches = [];
let newString = string.replace(pattern, function() {
const match = [...arguments].slice(0, -2);
match.input = arguments[arguments.length - 1];
match.index = arguments[arguments.length - 2];
matches.push(match);
return `ES${2009 + Number(match[2])}`;
});
console.log(newString);
// ES2016 ES2017 ES2018 ECMAScript
console.log(matches);
// [
// ["ES7", "ES7", "7", input: "ES7 ES8 ES9 ECMAScript", index: 0],
// ["ES8", "ES8", "8", input: "ES7 ES8 ES9 ECMAScript", index: 4],
// ["ES9", "ES9", "9", input: "ES7 ES8 ES9 ECMAScript", index: 8]
// ]
但太麻煩了...
String.prototype.matchAll()let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let matches = string.matchAll(pattern);
console.log(matches);
// RegExpStringIterator {}
for (const match of matches) {
console.log(match);
console.log(`lastIndex: ${pattern.lastIndex}`);
}
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// lastIndex: 0
// ["ES8", "ES8", "8", index: 4, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// lastIndex: 0
// ["ES9", "ES9", "9", index: 8, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// lastIndex: 0
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let matches = [...string.matchAll(pattern)];
console.log(matches);
// [
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined],
// ["ES8", "ES8", "8", index: 4, input: "ES7 ES8 ES9 ECMAScript", groups: undefined],
// ["ES9", "ES9", "9", index: 8, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// ]
RegExp) | JavaScript for impatient programmers (ES2020 edition)